问题:无障碍快捷方式(Accessibility Shortcut)打开不生效。
如图,打开功能后,长按power键会出现振动,震动后双指放在屏幕上会打开无障碍。
无障碍的功能从来没有接触过,也不清楚在哪个模块修改,所以下面记录一下如何快速定位这种问题的思路:
在Opengrok检索"Accessibility Shortcut"
找到字串accessibility_global_gesture_preference_title
,可以确定两个地方:
- Settings里Accessibility选项的入口
packages/apps/Settings/res/xml/accessibility_settings.xml
- Accessibility的控制代码
packages/apps/Settings/src/com/android/settings/accessibility/AccessibilitySettings.java
AccessibilitySettings.java
通过阅读方法列表,知道这个类完全是用来控制Settings的Accessibility界面按钮的逻辑。
从控制代码,知道打开上图界面的代码是:
private void handleToggleEnableAccessibilityGesturePreferenceClick() {
Bundle extras = mGlobalGesturePreferenceScreen.getExtras();
extras.putString(EXTRA_TITLE, getString(
R.string.accessibility_global_gesture_preference_title));
extras.putString(EXTRA_SUMMARY, getString(
R.string.accessibility_global_gesture_preference_description));
extras.putBoolean(EXTRA_CHECKED, Settings.Global.getInt(getContentResolver(),Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1);
super.onPreferenceTreeClick(mGlobalGesturePreferenceScreen);
}
这一步是设置Title和Summary,和设置SwichBar是否check。所以这个KEY是控制变量的关键。
搜索Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED
,可以了解到几个地方:
ToggleGlobalGesturePreferenceFragment.java
这里通过KEY的命名和相关类的命名,可以知道,Accessibility Shortcut打开后,相关的手势被称为Global Gesture
,全局手势。了解命名也很重要,这对于分析不熟悉的模块很有帮助。
这个Fragment是处理SwichBar的回调。每当SwichBar状态变化,就会更新相关的值:
@Override
protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
Settings.Global.putInt(getContentResolver(),
Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, enabled ? 1 : 0);
}
前面两个类似乎并不是关键,后面的两个就不一样了。
PhoneWindowManager.java
这是个大类,涉及到非常多的控制逻辑,而且从命名就能知道它的核心功能。但实际上,对于这个问题,只需要看一部分逻辑:
Accessibility Shortcut功能是长按POWER键启用。在PWM中,有powerLongPress
的处理逻辑:
private void powerLongPress() {
final int behavior = getResolvedLongPressOnPowerBehavior();
switch (behavior) {
case LONG_PRESS_POWER_NOTHING:
break;
case LONG_PRESS_POWER_GLOBAL_ACTIONS:
mPowerKeyHandled = true;
if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) {
performAuditoryFeedbackForAccessibilityIfNeed();
}
showGlobalActionsInternal();
break;
case LONG_PRESS_POWER_SHUT_OFF:
case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM:
mPowerKeyHandled = true;
performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF);
break;
}
}
首先获取长按POWER键的行为:
private int getResolvedLongPressOnPowerBehavior() {
if (FactoryTest.isLongPressOnPowerOffEnabled()) {
return LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM;
}
return mLongPressOnPowerBehavior;
}
正常情况下都会直接返回mLongPressOnPowerBehavior
,这个变量初始化如下:
mLongPressOnPowerBehavior = mContext.getResources().getInteger(
com.android.internal.R.integer.config_longPressOnPowerBehavior);
这个值在frameworks/base/core/res/res/values/config.xml
配置为1。所以在Switch会走到LONG_PRESS_POWER_GLOBAL_ACTIONS这个case。
在这个case下,首先会调用performHapticFeedbackLW方法,从名称看,是perform触觉反馈LW方法,最终目的是发出振动。
@Override
public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always) {
if (!mVibrator.hasVibrator()) {
return false;
}
final boolean hapticsDisabled = Settings.System.getIntForUser(mContext.getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
if (hapticsDisabled && !always) {
return false;
}
long[] pattern = null;
switch (effectId) {
case HapticFeedbackConstants.LONG_PRESS:
pattern = mLongPressVibePattern;
break;
case HapticFeedbackConstants.VIRTUAL_KEY:
pattern = mVirtualKeyVibePattern;
break;
case HapticFeedbackConstants.KEYBOARD_TAP:
pattern = mKeyboardTapVibePattern;
break;
case HapticFeedbackConstants.CLOCK_TICK:
pattern = mClockTickVibePattern;
break;
case HapticFeedbackConstants.CALENDAR_DATE:
pattern = mCalendarDateVibePattern;
break;
case HapticFeedbackConstants.SAFE_MODE_DISABLED:
pattern = mSafeModeDisabledVibePattern;
break;
case HapticFeedbackConstants.SAFE_MODE_ENABLED:
pattern = mSafeModeEnabledVibePattern;
break;
case HapticFeedbackConstants.CONTEXT_CLICK:
pattern = mContextClickVibePattern;
break;
default:
return false;
}
int owningUid;
String owningPackage;
if (win != null) {
owningUid = win.getOwningUid();
owningPackage = win.getOwningPackage();
} else {
owningUid = android.os.Process.myUid();
owningPackage = mContext.getOpPackageName();
}
if (pattern.length == 1) {
// One-shot vibration
mVibrator.vibrate(owningUid, owningPackage, pattern[0], VIBRATION_ATTRIBUTES);
} else {
// Pattern vibration
mVibrator.vibrate(owningUid, owningPackage, pattern, -1, VIBRATION_ATTRIBUTES);
}
return true;
}
如果上面返回为false,则没有振动,代码会进入performAuditoryFeedbackForAccessibilityIfNeed
,即播放声音。
private void performAuditoryFeedbackForAccessibilityIfNeed() {
if (!isGlobalAccessibilityGestureEnabled()) {
return;
}
AudioManager audioManager = (AudioManager) mContext.getSystemService(
Context.AUDIO_SERVICE);
if (audioManager.isSilentMode()) {
return;
}
Ringtone ringTone = RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
ringTone.setStreamType(AudioManager.STREAM_MUSIC);
ringTone.play();
}
播放声音前的判断就是 通过获取标志位判断。
private boolean isGlobalAccessibilityGestureEnabled() {
return Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1;
}
最后调用的是showGlobalActionsInternal方法
void showGlobalActionsInternal() {
sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
if (mGlobalActions == null) {
mGlobalActions = new GlobalActions(mContext, mWindowManagerFuncs);
}
final boolean keyguardShowing = isKeyguardShowingAndNotOccluded();
mGlobalActions.showDialog(keyguardShowing, isDeviceProvisioned());
if (keyguardShowing) {
// since it took two seconds of long press to bring this up,
// poke the wake lock so they have some time to see the dialog.
mPowerManager.userActivity(SystemClock.uptimeMillis(), false);
}
}
所以这里看出,PWM只是处理了长按power键的逻辑。那长按之后,双指触摸屏幕的逻辑在哪控制呢?
GlobalActions.java
这个类蛮有意思的,通过类名知道他是全局的一个操作,里面出现大量的控制代码,而且很多和手势操作相关。有机会可以深入了解一下。
在onStart的地方,通过阅读注释,可以知道,这里处理打开Accessibility后的双指触摸,触摸时不销毁弹出的关机选项对话框。
代码中有一个非常重要的判断,它决定是否进入Accessibility模式。
@Override
protected void onStart() {
// If global accessibility gesture can be performed, we will take care
// of dismissing the dialog on touch outside. This is because the dialog
// is dismissed on the first down while the global gesture is a long press
// with two fingers anywhere on the screen.
if (EnableAccessibilityController.canEnableAccessibilityViaGesture(mContext)) {
mEnableAccessibilityController = new EnableAccessibilityController(mContext,
new Runnable() {
@Override
public void run() {
dismiss();
}
});
super.setCanceledOnTouchOutside(false);
} else {
mEnableAccessibilityController = null;
super.setCanceledOnTouchOutside(true);
}
super.onStart();
}
EnableAccessibilityController.java
public static boolean canEnableAccessibilityViaGesture(Context context) {
AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(context);
// Accessibility is enabled and there is an enabled speaking
// accessibility service, then we have nothing to do.
if (accessibilityManager.isEnabled()
&& !accessibilityManager.getEnabledAccessibilityServiceList(
AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty()) {
return false;
}
// If the global gesture is enabled and there is a speaking service
// installed we are good to go, otherwise there is nothing to do.
return Settings.Global.getInt(context.getContentResolver(),
Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1
&& !getInstalledSpeakingAccessibilityServices(context).isEmpty();
}
这里需要判断几层:
- AccessibilityManager enable
- AccessibilityManager 服务不为空。这里服务可以是自定义安装的。如果没有可用服务就无从打开。
- ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED标志位打开,即前面界面的switch按钮打开。
- talkbak必须安装。因为他需要语音播放一些内容,所以talkbak是必备的。
我遇到的问题就是手机没有集成GMS talkbak,导致Accessibility打开没反应。